package com.javaweb.filter;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.ServletRequestPathUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.javaweb.base.BaseInject;
import com.javaweb.base.BaseSystemMemory;
import com.javaweb.base.po.Interfaces;
import com.javaweb.constant.CommonConstant;
import com.javaweb.constant.SystemConstant;
import com.javaweb.constant.SystemConstant.InterfaceLimit;
import com.javaweb.constant.SystemConstant.RedisConstant;
import com.javaweb.exception.ServiceException;
import com.javaweb.exception.TokenExpiredException;
import com.javaweb.util.core.HttpUtil;
import com.javaweb.util.core.StringUtil;
import com.javaweb.web.eo.SecretRequest;
import com.javaweb.web.eo.TokenData;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

/** 防止表单重复提交切面（此功能已被本类InterfaceLimitFilter替代）
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventResubmit {

}

@Aspect
@Component
public class PreventResubmitAspect extends BaseInject {
	
	@Pointcut("@annotation(preventResubmit)")
	public void pointcut(PreventResubmit preventResubmit) { 
		//do nothing
	}
	
	@Around("pointcut(preventResubmit)")
	public Object arround(ProceedingJoinPoint proceedingJoinPoint,PreventResubmit preventResubmit) throws Throwable {
		ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
		HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
		String url = httpServletRequest.getRequestURI();//url
		String parameter = httpServletRequest.getQueryString();//url后面的参数
		parameter = (parameter==null?CommonConstant.EMPTY_VALUE:parameter.strip());
		Object[] body = proceedingJoinPoint.getArgs();//body
		String newBody = ((body==null||body.length==0)?CommonConstant.EMPTY_VALUE:body.toString());//这里参数极其简单的处理了，如有需要请自行详细处理
		log.info("防止表单重复提交，接收到的参数为：url["+url+"],parameter["+parameter+"],body["+newBody+"]");
		Interfaces interfaces = (Interfaces)httpServletRequest.getAttribute(SystemConstant.REQUEST_URL_CHAIN_ATTRIBUTE_NAME);
		if(interfaces!=null){
			String key = CommonConstant.EMPTY_VALUE;
			TokenData tokenData = BaseInject.getTokenData(BaseInject.getToken(httpServletRequest));
			if((tokenData==null)||(tokenData.getUser()==null)||(Objects.equals(CommonConstant.EMPTY_VALUE,StringUtil.handleNullString(tokenData.getUser().getUserId())))){
				key = HttpUtil.getIpAddress(httpServletRequest) + CommonConstant.POUND + (url+parameter+newBody);//禁IP
			}else{
				key = tokenData.getUser().getUserId() + CommonConstant.POUND + (url+parameter+newBody);//禁用户
			}
			Boolean isSuccess = valueOperations.setIfAbsent(key,1,3,TimeUnit.SECONDS);//可以比如设置在3秒内不能重复提交表单
			if(isSuccess!=null&&isSuccess){
				return proceedingJoinPoint.proceed();
			}else{
				return getBaseResponseResult(HttpCodeEnum.PREVENT_RE_SUBMIT,"validated.permission.preventReSubmit");//表单重复提交了
			}
		}
		return proceedingJoinPoint.proceed();
	}

}
*/
/**
 * 接口限流过滤器
 * <p>
 * Order(1)+WebFilter(filterName="xxx",urlPatterns={xxx})+Component组合的注解写法在某些版本或情况下是无效的，有时可能由于配置不当甚至报错
 * @see InterfaceLimitFilterConfig#interfaceLimitFilter
 * @author tank2140896
 */
public class InterfaceLimitFilter implements Filter {
	
	/**
	 * slf4j日志
	 */
	private final Logger log = LoggerFactory.getLogger(this.getClass());
	
	/**
	 * 初始化方法
	 */
	public void init(FilterConfig filterConfig) throws ServletException {
		//do nothing
	}
	
	/**
	 * 销毁方法
	 */
    public void destroy() {
    	//do nothing
    }
	
	/**
	 * 过滤器方法
	 * <p>
	 * httpServletRequest.getRequestURI()            ------------------------- /JavaWebAdmin/app/html/home.html
	 * httpServletRequest.getRequestURL().toString() ------------------------- http://localhost:8080/JavaWebAdmin/app/html/home.html 
	 * httpServletRequest.getServletPath()           ------------------------- /app/html/home.html
	 * request.getRequestDispatcher("/test").forward(request,response) ------- 服务端跳转
	 * response.sendRedirect(basePath+"/test") ------------------------------- 页面端跳转
	 * System.getProperty("catalina.home")+File.separator+"webapps\\项目名称\\WEB-INF\\classes"
	 * request.getSession().getServletContext().getRealPath("/")//位于WebRoot下
	 * request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+request.getContextPath();
	 */
	public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletException {
		HttpServletRequest httpServletRequest = (HttpServletRequest)request;
		//HttpServletResponse httpServletResponse = (HttpServletResponse)response;
		if(SystemConstant.HTTP_REQUEST_METHOD_OPTIONS.equalsIgnoreCase(httpServletRequest.getMethod())) {//OPTIONS请求放行
			chain.doFilter(request,response);
			return;
		}
		ServletRequestPathUtils.parseAndCache(httpServletRequest);//较新版本需要加上这行代码
		Interfaces interfaces = BaseSystemMemory.getMatchRequestMapping(httpServletRequest);//虽然需要遍历匹配但这是最准确的路径匹配方式
		if(interfaces!=null) {
			request.setAttribute(SystemConstant.REQUEST_URL_CHAIN_ATTRIBUTE_NAME,interfaces);//便于一次请求内的链路取值
			//String url = httpServletRequest.getServletPath();//依照请求URL来限流这里并不推荐，尤其碰到一些通配URL，如/a/{x}/b/{y}或/a?id=1、/a?id=2、/a?id=2，另外需要注意，?后面的参数需要用getQueryString方法获取
			String url = interfaces.getUrl();//这里获得的URL为通配，形如：/web/sys/config/list或/web/sys/dictionary/delete/{dictionaryId}
			String limitKey = getLimitKey(url,httpServletRequest);
			BaseInject.getRedisTemplate().opsForHash().increment(RedisConstant.REDIS_INTERFACE_COUNT_KEY,url,1);//接口调用次数加1（接口调用次数统计，若不需要注释掉即可）
			TimeUnit timeUnit = needLimit(interfaces);//是否接口限流
			if(timeUnit!=null){
				boolean isSuccess = interfaceCurrentLimiting(limitKey,timeUnit,Integer.parseInt(interfaces.getTimes()),Integer.parseInt(interfaces.getCounts()));
				if(isSuccess){
					chain.doFilter(getWrappedRequest(request,interfaces),response);//chain.doFilter(request,response);
				}else{
					httpServletRequest.getRequestDispatcher("/requestLimit").forward(request,response);
				}
			}else{
				chain.doFilter(getWrappedRequest(request,interfaces),response);//chain.doFilter(request,response);
			}
		}else{
			//没有匹配到的可能有些接口来自jar包引用内或websocket等，因此需要做一个例外排除
			if(InterfaceLimit.EXCLUSIVE_URL_PATTERN.matcher(httpServletRequest.getServletPath()).matches()) {
				chain.doFilter(request,response);
			}else {
				httpServletRequest.getRequestDispatcher("/notFound").forward(request,response);
			}
		}
	}
	
	/* -------------------------------------------------- 分界线 -------------------------------------------------- */
    
    /**
     * 获取接口限流key
     * @param url 请求的URL通配
     * @param httpServletRequest HttpServletRequest
     * @return String 接口限流key
     */
    private String getLimitKey(String url,HttpServletRequest httpServletRequest){
    	String key = CommonConstant.POUND + url;//# + url
		TokenData tokenData = BaseInject.getTokenData(BaseInject.getToken(httpServletRequest));
		try {
			key = tokenData.getUser().getUserId() + key;//禁用户
		} catch (Exception e) {
			key = HttpUtil.getIpAddress(httpServletRequest) + key;//禁IP
		}
		log.info("接口限流key值为："+key);
		return key;
    }
    
    /**
     * 判断是否需要限流
     * @param interfaces Interfaces
     * @return TimeUnit 为null则表示不需要限流，不为null则表示需要限流
     */
    private TimeUnit needLimit(Interfaces interfaces){
    	String time = interfaces.getTimes();//时间
		String unit = interfaces.getUnit();//单位
		String counts = interfaces.getCounts();//次数
    	TimeUnit timeUnit = null;
    	try{
    		//System.out.println(String.join(",",time,unit,counts));
        	long nullCount = Stream.of(time,unit,counts).filter(e->StringUtil.isEmpty(e)).count();
        	//System.out.println(nullCount);
        	if(0==nullCount){
        		if(Integer.parseInt(counts)<=Integer.MAX_VALUE){
        			if(unit.matches("[时分秒]")){//目前只支持秒分时
            			if("时".equals(unit)&&(Integer.parseInt(time)>0&&Integer.parseInt(time)<=24)){
            				timeUnit = TimeUnit.HOURS;
            			}else if("分".equals(unit)&&(Integer.parseInt(time)>0&&Integer.parseInt(time)<=60)){
            				timeUnit = TimeUnit.MINUTES;
            			}else if("秒".equals(unit)&&(Integer.parseInt(time)>0&&Integer.parseInt(time)<=60)){
            				timeUnit = TimeUnit.SECONDS;
            			}
            		}
        		}
        	}
    	}catch(Exception e){
    		//do nothing
    	}
    	return timeUnit;
    }
    
    /**
     * 接口限流处理
     * @param key 限流key
     * @param timeUnit TimeUnit
     * @param times 时间
     * @param counts 次数
     * @return boolean true表示不需要限流，false表示需要限流
     */
    private boolean interfaceCurrentLimiting(String key,TimeUnit timeUnit,Integer times,Integer counts) {//接口限流
		synchronized(key){
			if(BaseInject.getRedisTemplate().hasKey(key)){//记录过
				int count = (int)BaseInject.getRedisTemplate().opsForValue().get(key);
				if(count>=counts){
					BaseInject.getRedisTemplate().opsForValue().set(key,counts+1,times,timeUnit);//频繁调用封禁
					return false;
				}else{
					BaseInject.getRedisTemplate().opsForValue().increment(key,1);
				}
			}else{//没有记录过
				BaseInject.getRedisTemplate().opsForValue().set(key,1,times,timeUnit);
			}
			return true;
		}
    }
    
    /**
     * 请求流二次适配
     * <p>
     * 整体分为json和非json请求
     * json请求如需加密，GET和DELETE需要校验header；POST和PUT需要校验body
     * 非json请求如需加密，GET、DELETE、POST、和PUT均只需校验header
     * @param servletRequest ServletRequest
     * @param interfaces 接口信息
     * @return ServletRequest ServletRequest
     */
    private ServletRequest getWrappedRequest(ServletRequest servletRequest,Interfaces interfaces) {
    	HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
    	//参见SecretRequest：1（application/json）；2（multipart/form-data）；3（application/x-www-form-urlencoded）
    	String requestContentType = httpServletRequest.getHeader("requestContentType");
    	log.info("接口通配："+interfaces.getUrl()+","+(interfaces.getRequestDataSecret()==1?"请求参数需要加密":"请求参数不需要加密"));
		if("1".equals(requestContentType)){//json
	    	if(interfaces.getRequestDataSecret()==1){//0：不加密；1：加密
	    		String method = interfaces.getMethod();
	    		if("GET".equals(method)||"DELETE".equals(method)) {
	                TokenData tokenData = BaseInject.getTokenData(BaseInject.getToken(httpServletRequest));
	        		if(tokenData==null){
	        			throw new TokenExpiredException();
	        		}
	                if(new SecretRequest().rsaCheckPass(httpServletRequest,tokenData)) {
	                	return httpServletRequest;
	                }else{
	                	throw new ServiceException("解密错误");
	                }
	    		}else {
	    			try{
	    				WrappedRequest wrappedRequest = new WrappedRequest(httpServletRequest);
	    				return wrappedRequest;
	    			}catch(Exception e){
	    				throw new ServiceException(e.getMessage());
	    			}
	    		}
	    	}
		} else {//非json
			if(interfaces.getRequestDataSecret()==1){//0：不加密；1：加密
                TokenData tokenData = BaseInject.getTokenData(BaseInject.getToken(httpServletRequest));
        		if(tokenData==null){
        			throw new TokenExpiredException();
        		}
                if(new SecretRequest().rsaCheckPass(httpServletRequest,tokenData)) {
                	return httpServletRequest;
                }else{
                	throw new ServiceException("解密错误");
                }
			}
		}
    	return httpServletRequest;
    }
    
    class WrappedRequest extends HttpServletRequestWrapper {

    	private String myBody;        

        public WrappedRequest(HttpServletRequest request) throws Exception {
            super(request);
            this.myBody = this.getBody(request);
        }

        private String getBody(HttpServletRequest httpServletRequest) throws Exception {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpServletRequest.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            try {
                while((line=bufferedReader.readLine())!=null) {
                	stringBuilder.append(line);
                }
            } catch (IOException e) {
                //do nothing
            }
            line = stringBuilder.toString();
            //log.info("未解密前的请求参数为："+line);
            SecretRequest secretRequest = new ObjectMapper().readValue(line,SecretRequest.class);
            TokenData tokenData = BaseInject.getTokenData(BaseInject.getToken(httpServletRequest));
    		if(tokenData==null){
    			throw new TokenExpiredException();
    		}
            if(secretRequest.rsaCheckPass(httpServletRequest,tokenData)) {
            	//log.info("解密后的请求参数为："+secretRequest.getCode());
            	return secretRequest.getCode();
            }else{
            	throw new ServiceException("解密错误");
            }
        }

        public String getBody() {
            return this.myBody;
        }

        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(this.getInputStream()));
        }

        public ServletInputStream getInputStream() throws IOException {
            final var byteArrayInputStream = new ByteArrayInputStream(this.myBody.getBytes(StandardCharsets.UTF_8));
            return new ServletInputStream() {
            	
                public boolean isFinished() {
                    return false;
                }
                
                public boolean isReady() {
                    return false;
                }
                
                public void setReadListener(ReadListener readListener) {
                	//do nothing
                }
                
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }
                
            };
            
        }
    }

}
